// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

package waveappstore

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/wavetermdev/waveterm/pkg/secretstore"
	"github.com/wavetermdev/waveterm/pkg/util/fileutil"
	"github.com/wavetermdev/waveterm/pkg/waveapputil"
	"github.com/wavetermdev/waveterm/pkg/wavebase"
	"github.com/wavetermdev/waveterm/pkg/wshrpc"
)

const (
	AppNSLocal = "local"
	AppNSDraft = "draft"

	MaxNamespaceLen = 30
	MaxAppNameLen   = 50

	ManifestFileName       = "manifest.json"
	SecretBindingsFileName = "secret-bindings.json"
)

var (
	namespaceRegex = regexp.MustCompile(`^@?[a-z0-9-]+$`)
	appNameRegex   = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
)

type FileData struct {
	Contents []byte
	ModTs    int64
}

func MakeAppId(appNS string, appName string) string {
	return appNS + "/" + appName
}

func ParseAppId(appId string) (appNS string, appName string, err error) {
	parts := strings.Split(appId, "/")
	if len(parts) != 2 {
		return "", "", fmt.Errorf("invalid appId format: must be namespace/name")
	}
	appNS = parts[0]
	appName = parts[1]
	if appNS == "" || appName == "" {
		return "", "", fmt.Errorf("invalid appId: namespace and name cannot be empty")
	}
	return appNS, appName, nil
}

func ValidateAppId(appId string) error {
	appNS, appName, err := ParseAppId(appId)
	if err != nil {
		return err
	}
	if len(appNS) > MaxNamespaceLen {
		return fmt.Errorf("namespace too long: max %d characters", MaxNamespaceLen)
	}
	if len(appName) > MaxAppNameLen {
		return fmt.Errorf("app name too long: max %d characters", MaxAppNameLen)
	}
	if !namespaceRegex.MatchString(appNS) {
		return fmt.Errorf("invalid namespace: must match pattern @?[a-z0-9-]+")
	}
	if !appNameRegex.MatchString(appName) {
		return fmt.Errorf("invalid app name: must match pattern [a-zA-Z0-9_-]+")
	}
	return nil
}

func GetAppDir(appId string) (string, error) {
	if err := ValidateAppId(appId); err != nil {
		return "", err
	}
	appNS, appName, _ := ParseAppId(appId)
	homeDir := wavebase.GetHomeDir()
	return filepath.Join(homeDir, "waveapps", appNS, appName), nil
}

func copyDir(src, dst string) error {
	if err := os.RemoveAll(dst); err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("failed to remove existing directory: %w", err)
	}
	if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
		return fmt.Errorf("failed to create parent directory: %w", err)
	}

	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		relPath, err := filepath.Rel(src, path)
		if err != nil {
			return err
		}
		dstPath := filepath.Join(dst, relPath)

		if info.IsDir() {
			return os.MkdirAll(dstPath, info.Mode())
		}

		data, err := os.ReadFile(path)
		if err != nil {
			return err
		}
		return os.WriteFile(dstPath, data, info.Mode())
	})
}

func PublishDraft(draftAppId string) (string, error) {
	if err := ValidateAppId(draftAppId); err != nil {
		return "", fmt.Errorf("invalid appId: %w", err)
	}

	appNS, appName, _ := ParseAppId(draftAppId)
	if appNS != AppNSDraft {
		return "", fmt.Errorf("appId must be in draft namespace, got: %s", appNS)
	}

	draftDir, err := GetAppDir(draftAppId)
	if err != nil {
		return "", err
	}

	if _, err := os.Stat(draftDir); os.IsNotExist(err) {
		return "", fmt.Errorf("draft app does not exist: %s", draftDir)
	}

	localAppId := MakeAppId(AppNSLocal, appName)
	localDir, err := GetAppDir(localAppId)
	if err != nil {
		return "", err
	}

	if err := copyDir(draftDir, localDir); err != nil {
		return "", err
	}

	return localAppId, nil
}

func RevertDraft(draftAppId string) error {
	if err := ValidateAppId(draftAppId); err != nil {
		return fmt.Errorf("invalid appId: %w", err)
	}

	appNS, appName, _ := ParseAppId(draftAppId)
	if appNS != AppNSDraft {
		return fmt.Errorf("appId must be in draft namespace, got: %s", appNS)
	}

	draftDir, err := GetAppDir(draftAppId)
	if err != nil {
		return err
	}

	localAppId := MakeAppId(AppNSLocal, appName)
	localDir, err := GetAppDir(localAppId)
	if err != nil {
		return err
	}

	if _, err := os.Stat(localDir); os.IsNotExist(err) {
		return fmt.Errorf("local app does not exist: %s", localDir)
	}

	return copyDir(localDir, draftDir)
}

func MakeDraftFromLocal(localAppId string) (string, error) {
	if err := ValidateAppId(localAppId); err != nil {
		return "", fmt.Errorf("invalid appId: %w", err)
	}

	appNS, appName, _ := ParseAppId(localAppId)
	if appNS != AppNSLocal {
		return "", fmt.Errorf("appId must be in local namespace, got: %s", appNS)
	}

	localDir, err := GetAppDir(localAppId)
	if err != nil {
		return "", err
	}

	if _, err := os.Stat(localDir); os.IsNotExist(err) {
		return "", fmt.Errorf("local app does not exist: %s", localDir)
	}

	draftAppId := MakeAppId(AppNSDraft, appName)
	draftDir, err := GetAppDir(draftAppId)
	if err != nil {
		return "", err
	}

	if _, err := os.Stat(draftDir); err == nil {
		// draft already exists, don't overwrite (that's what RevertDraft is for)
		return draftAppId, nil
	} else if !os.IsNotExist(err) {
		return "", err
	}

	if err := copyDir(localDir, draftDir); err != nil {
		return "", err
	}

	return draftAppId, nil
}

func DeleteApp(appId string) error {
	if err := ValidateAppId(appId); err != nil {
		return fmt.Errorf("invalid appId: %w", err)
	}

	appDir, err := GetAppDir(appId)
	if err != nil {
		return err
	}

	if err := os.RemoveAll(appDir); err != nil {
		return fmt.Errorf("failed to delete app directory: %w", err)
	}

	return nil
}

func validateAndResolveFilePath(appDir string, fileName string) (string, error) {
	if filepath.IsAbs(fileName) {
		return "", fmt.Errorf("fileName must be relative, got absolute path: %s", fileName)
	}

	cleanPath := filepath.Clean(fileName)
	if strings.HasPrefix(cleanPath, "..") || strings.Contains(cleanPath, string(filepath.Separator)+"..") {
		return "", fmt.Errorf("path traversal not allowed: %s", fileName)
	}

	fullPath := filepath.Join(appDir, cleanPath)
	resolvedPath, err := filepath.Abs(fullPath)
	if err != nil {
		return "", fmt.Errorf("failed to resolve path: %w", err)
	}

	resolvedAppDir, err := filepath.Abs(appDir)
	if err != nil {
		return "", fmt.Errorf("failed to resolve app directory: %w", err)
	}

	if !strings.HasPrefix(resolvedPath, resolvedAppDir+string(filepath.Separator)) && resolvedPath != resolvedAppDir {
		return "", fmt.Errorf("path escapes app directory: %s", fileName)
	}

	return resolvedPath, nil
}

func WriteAppFile(appId string, fileName string, contents []byte) error {
	if err := ValidateAppId(appId); err != nil {
		return fmt.Errorf("invalid appId: %w", err)
	}

	appDir, err := GetAppDir(appId)
	if err != nil {
		return err
	}

	filePath, err := validateAndResolveFilePath(appDir, fileName)
	if err != nil {
		return err
	}

	if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
		return fmt.Errorf("failed to create directory: %w", err)
	}

	if err := os.WriteFile(filePath, contents, 0644); err != nil {
		return fmt.Errorf("failed to write file: %w", err)
	}

	return nil
}

func ReadAppFile(appId string, fileName string) (*FileData, error) {
	if err := ValidateAppId(appId); err != nil {
		return nil, fmt.Errorf("invalid appId: %w", err)
	}

	appDir, err := GetAppDir(appId)
	if err != nil {
		return nil, err
	}

	filePath, err := validateAndResolveFilePath(appDir, fileName)
	if err != nil {
		return nil, err
	}

	fileInfo, err := os.Stat(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to stat file: %w", err)
	}

	contents, err := os.ReadFile(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to read file: %w", err)
	}

	return &FileData{
		Contents: contents,
		ModTs:    fileInfo.ModTime().UnixMilli(),
	}, nil
}

func DeleteAppFile(appId string, fileName string) error {
	if err := ValidateAppId(appId); err != nil {
		return fmt.Errorf("invalid appId: %w", err)
	}

	appDir, err := GetAppDir(appId)
	if err != nil {
		return err
	}

	filePath, err := validateAndResolveFilePath(appDir, fileName)
	if err != nil {
		return err
	}

	if err := os.Remove(filePath); err != nil {
		return fmt.Errorf("failed to delete file: %w", err)
	}

	return nil
}

func ReplaceInAppFile(appId string, fileName string, edits []fileutil.EditSpec) error {
	if err := ValidateAppId(appId); err != nil {
		return fmt.Errorf("invalid appId: %w", err)
	}

	appDir, err := GetAppDir(appId)
	if err != nil {
		return err
	}

	filePath, err := validateAndResolveFilePath(appDir, fileName)
	if err != nil {
		return err
	}

	return fileutil.ReplaceInFile(filePath, edits)
}

func ReplaceInAppFilePartial(appId string, fileName string, edits []fileutil.EditSpec) ([]fileutil.EditResult, error) {
	if err := ValidateAppId(appId); err != nil {
		return nil, fmt.Errorf("invalid appId: %w", err)
	}

	appDir, err := GetAppDir(appId)
	if err != nil {
		return nil, err
	}

	filePath, err := validateAndResolveFilePath(appDir, fileName)
	if err != nil {
		return nil, err
	}

	return fileutil.ReplaceInFilePartial(filePath, edits)
}

func RenameAppFile(appId string, fromFileName string, toFileName string) error {
	if err := ValidateAppId(appId); err != nil {
		return fmt.Errorf("invalid appId: %w", err)
	}

	appDir, err := GetAppDir(appId)
	if err != nil {
		return err
	}

	fromPath, err := validateAndResolveFilePath(appDir, fromFileName)
	if err != nil {
		return fmt.Errorf("invalid source path: %w", err)
	}

	toPath, err := validateAndResolveFilePath(appDir, toFileName)
	if err != nil {
		return fmt.Errorf("invalid destination path: %w", err)
	}

	if err := os.MkdirAll(filepath.Dir(toPath), 0755); err != nil {
		return fmt.Errorf("failed to create destination directory: %w", err)
	}

	if err := os.Rename(fromPath, toPath); err != nil {
		return fmt.Errorf("failed to rename file: %w", err)
	}

	return nil
}

func FormatGoFile(appId string, fileName string) error {
	if err := ValidateAppId(appId); err != nil {
		return fmt.Errorf("invalid appId: %w", err)
	}

	appDir, err := GetAppDir(appId)
	if err != nil {
		return err
	}

	filePath, err := validateAndResolveFilePath(appDir, fileName)
	if err != nil {
		return err
	}

	if filepath.Ext(filePath) != ".go" {
		return fmt.Errorf("file is not a Go file: %s", fileName)
	}

	gofmtPath, err := waveapputil.ResolveGoFmtPath()
	if err != nil {
		return fmt.Errorf("failed to resolve gofmt path: %w", err)
	}

	cmd := exec.Command(gofmtPath, "-w", filePath)
	if output, err := cmd.CombinedOutput(); err != nil {
		return fmt.Errorf("gofmt failed: %w\nOutput: %s", err, string(output))
	}

	return nil
}

func ListAllAppFiles(appId string) (*fileutil.ReadDirResult, error) {
	if err := ValidateAppId(appId); err != nil {
		return nil, fmt.Errorf("invalid appId: %w", err)
	}

	appDir, err := GetAppDir(appId)
	if err != nil {
		return nil, err
	}

	if _, err := os.Stat(appDir); os.IsNotExist(err) {
		return nil, fmt.Errorf("app directory does not exist: %s", appDir)
	}

	return fileutil.ReadDirRecursive(appDir, 10000)
}

func ListAllApps() ([]wshrpc.AppInfo, error) {
	homeDir := wavebase.GetHomeDir()
	waveappsDir := filepath.Join(homeDir, "waveapps")

	if _, err := os.Stat(waveappsDir); os.IsNotExist(err) {
		return []wshrpc.AppInfo{}, nil
	}

	namespaces, err := os.ReadDir(waveappsDir)
	if err != nil {
		return nil, fmt.Errorf("failed to read waveapps directory: %w", err)
	}

	var appInfos []wshrpc.AppInfo

	for _, ns := range namespaces {
		if !ns.IsDir() {
			continue
		}

		namespace := ns.Name()
		nsPath := filepath.Join(waveappsDir, namespace)

		apps, err := os.ReadDir(nsPath)
		if err != nil {
			continue
		}

		for _, app := range apps {
			if !app.IsDir() {
				continue
			}

			appName := app.Name()
			appId := MakeAppId(namespace, appName)

			if err := ValidateAppId(appId); err == nil {
				modTime, _ := GetAppModTime(appId)
				appInfo := wshrpc.AppInfo{
					AppId:   appId,
					ModTime: modTime,
				}

				if manifest, err := ReadAppManifest(appId); err == nil {
					appInfo.Manifest = manifest
				}

				appInfos = append(appInfos, appInfo)
			}
		}
	}

	return appInfos, nil
}

func GetAppModTime(appId string) (int64, error) {
	if err := ValidateAppId(appId); err != nil {
		return 0, err
	}

	homeDir := wavebase.GetHomeDir()
	appNS, appName, err := ParseAppId(appId)
	if err != nil {
		return 0, err
	}

	appPath := filepath.Join(homeDir, "waveapps", appNS, appName)
	appGoPath := filepath.Join(appPath, "app.go")

	fileInfo, err := os.Stat(appGoPath)
	if err == nil {
		return fileInfo.ModTime().UnixMilli(), nil
	}

	dirInfo, err := os.Stat(appPath)
	if err != nil {
		return 0, nil
	}

	return dirInfo.ModTime().UnixMilli(), nil
}

func ListAllEditableApps() ([]wshrpc.AppInfo, error) {
	homeDir := wavebase.GetHomeDir()
	waveappsDir := filepath.Join(homeDir, "waveapps")

	if _, err := os.Stat(waveappsDir); os.IsNotExist(err) {
		return []wshrpc.AppInfo{}, nil
	}

	localApps := make(map[string]bool)
	draftApps := make(map[string]bool)

	localPath := filepath.Join(waveappsDir, AppNSLocal)
	if localEntries, err := os.ReadDir(localPath); err == nil {
		for _, app := range localEntries {
			if app.IsDir() {
				appName := app.Name()
				appId := MakeAppId(AppNSLocal, appName)
				if err := ValidateAppId(appId); err == nil {
					localApps[appName] = true
				}
			}
		}
	}

	draftPath := filepath.Join(waveappsDir, AppNSDraft)
	if draftEntries, err := os.ReadDir(draftPath); err == nil {
		for _, app := range draftEntries {
			if app.IsDir() {
				appName := app.Name()
				appId := MakeAppId(AppNSDraft, appName)
				if err := ValidateAppId(appId); err == nil {
					draftApps[appName] = true
				}
			}
		}
	}

	allAppNames := make(map[string]bool)
	for appName := range localApps {
		allAppNames[appName] = true
	}
	for appName := range draftApps {
		allAppNames[appName] = true
	}

	var appInfos []wshrpc.AppInfo
	for appName := range allAppNames {
		var appId string
		var manifestAppId string
		if localApps[appName] {
			appId = MakeAppId(AppNSLocal, appName)
		} else {
			appId = MakeAppId(AppNSDraft, appName)
		}

		if draftApps[appName] {
			manifestAppId = MakeAppId(AppNSDraft, appName)
		} else {
			manifestAppId = appId
		}

		modTime, _ := GetAppModTime(manifestAppId)

		appInfo := wshrpc.AppInfo{
			AppId:   appId,
			ModTime: modTime,
		}

		if manifest, err := ReadAppManifest(manifestAppId); err == nil {
			appInfo.Manifest = manifest
		}

		appInfos = append(appInfos, appInfo)
	}

	return appInfos, nil
}

func DraftHasLocalVersion(draftAppId string) (bool, error) {
	if err := ValidateAppId(draftAppId); err != nil {
		return false, fmt.Errorf("invalid appId: %w", err)
	}

	appNS, appName, _ := ParseAppId(draftAppId)
	if appNS != AppNSDraft {
		return false, fmt.Errorf("appId must be in draft namespace, got: %s", appNS)
	}

	localAppId := MakeAppId(AppNSLocal, appName)
	localDir, err := GetAppDir(localAppId)
	if err != nil {
		return false, err
	}

	if _, err := os.Stat(localDir); os.IsNotExist(err) {
		return false, nil
	}

	return true, nil
}

// RenameLocalApp renames a local app by renaming its directories in both the local and draft namespaces.
// It takes the current app name and the new app name (without namespace prefixes).
// Both local/[appName] and draft/[appName] will be renamed if they exist.
// Returns an error if the app doesn't exist in either namespace, if the new name is invalid,
// or if the new name conflicts with an existing app.
func RenameLocalApp(appName string, newAppName string) error {
	// Validate the old app name by constructing a valid appId
	oldLocalAppId := MakeAppId(AppNSLocal, appName)
	if err := ValidateAppId(oldLocalAppId); err != nil {
		return fmt.Errorf("invalid app name: %w", err)
	}

	// Validate the new app name by constructing a valid appId
	newLocalAppId := MakeAppId(AppNSLocal, newAppName)
	if err := ValidateAppId(newLocalAppId); err != nil {
		return fmt.Errorf("invalid new app name: %w", err)
	}

	homeDir := wavebase.GetHomeDir()
	waveappsDir := filepath.Join(homeDir, "waveapps")

	oldLocalDir := filepath.Join(waveappsDir, AppNSLocal, appName)
	newLocalDir := filepath.Join(waveappsDir, AppNSLocal, newAppName)
	oldDraftDir := filepath.Join(waveappsDir, AppNSDraft, appName)
	newDraftDir := filepath.Join(waveappsDir, AppNSDraft, newAppName)

	// Check if at least one of the apps exists
	localExists := false
	draftExists := false
	if _, err := os.Stat(oldLocalDir); err == nil {
		localExists = true
	} else if !os.IsNotExist(err) {
		return fmt.Errorf("failed to check local app: %w", err)
	}

	if _, err := os.Stat(oldDraftDir); err == nil {
		draftExists = true
	} else if !os.IsNotExist(err) {
		return fmt.Errorf("failed to check draft app: %w", err)
	}

	if !localExists && !draftExists {
		return fmt.Errorf("app '%s' does not exist in local or draft namespace", appName)
	}

	// Check if new app name already exists in either namespace
	if _, err := os.Stat(newLocalDir); err == nil {
		return fmt.Errorf("local app '%s' already exists", newAppName)
	} else if !os.IsNotExist(err) {
		return fmt.Errorf("failed to check if new local app exists: %w", err)
	}

	if _, err := os.Stat(newDraftDir); err == nil {
		return fmt.Errorf("draft app '%s' already exists", newAppName)
	} else if !os.IsNotExist(err) {
		return fmt.Errorf("failed to check if new draft app exists: %w", err)
	}

	// Rename local app if it exists
	if localExists {
		if err := os.Rename(oldLocalDir, newLocalDir); err != nil {
			return fmt.Errorf("failed to rename local app: %w", err)
		}
	}

	// Rename draft app if it exists
	if draftExists {
		if err := os.Rename(oldDraftDir, newDraftDir); err != nil {
			// If local was renamed but draft fails, try to rollback local rename
			if localExists {
				if rollbackErr := os.Rename(newLocalDir, oldLocalDir); rollbackErr != nil {
					return fmt.Errorf("failed to rename draft app (and failed to rollback local rename: %v): %w", rollbackErr, err)
				}
			}
			return fmt.Errorf("failed to rename draft app: %w", err)
		}
	}

	return nil
}

func ReadAppManifest(appId string) (*wshrpc.AppManifest, error) {
	if err := ValidateAppId(appId); err != nil {
		return nil, fmt.Errorf("invalid appId: %w", err)
	}

	appDir, err := GetAppDir(appId)
	if err != nil {
		return nil, err
	}

	manifestPath := filepath.Join(appDir, ManifestFileName)
	data, err := os.ReadFile(manifestPath)
	if err != nil {
		return nil, fmt.Errorf("failed to read %s: %w", ManifestFileName, err)
	}

	var manifest wshrpc.AppManifest
	if err := json.Unmarshal(data, &manifest); err != nil {
		return nil, fmt.Errorf("failed to parse %s: %w", ManifestFileName, err)
	}

	return &manifest, nil
}

func ReadAppSecretBindings(appId string) (map[string]string, error) {
	if err := ValidateAppId(appId); err != nil {
		return nil, fmt.Errorf("invalid appId: %w", err)
	}

	appDir, err := GetAppDir(appId)
	if err != nil {
		return nil, err
	}

	bindingsPath := filepath.Join(appDir, SecretBindingsFileName)
	data, err := os.ReadFile(bindingsPath)
	if err != nil {
		if os.IsNotExist(err) {
			return make(map[string]string), nil
		}
		return nil, fmt.Errorf("failed to read %s: %w", SecretBindingsFileName, err)
	}

	var bindings map[string]string
	if err := json.Unmarshal(data, &bindings); err != nil {
		return nil, fmt.Errorf("failed to parse %s: %w", SecretBindingsFileName, err)
	}

	if bindings == nil {
		bindings = make(map[string]string)
	}

	return bindings, nil
}

func WriteAppSecretBindings(appId string, bindings map[string]string) error {
	if err := ValidateAppId(appId); err != nil {
		return fmt.Errorf("invalid appId: %w", err)
	}

	appDir, err := GetAppDir(appId)
	if err != nil {
		return err
	}

	if bindings == nil {
		bindings = make(map[string]string)
	}

	data, err := json.MarshalIndent(bindings, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal bindings: %w", err)
	}

	bindingsPath := filepath.Join(appDir, SecretBindingsFileName)
	if err := os.WriteFile(bindingsPath, data, 0644); err != nil {
		return fmt.Errorf("failed to write %s: %w", SecretBindingsFileName, err)
	}

	return nil
}

func BuildAppSecretEnv(appId string, manifest *wshrpc.AppManifest, bindings map[string]string) (map[string]string, error) {
	if manifest == nil {
		return make(map[string]string), nil
	}

	if bindings == nil {
		bindings = make(map[string]string)
	}

	secretEnv := make(map[string]string)

	for secretName, secretMeta := range manifest.Secrets {
		boundSecretName, hasBinding := bindings[secretName]

		if !secretMeta.Optional && !hasBinding {
			return nil, fmt.Errorf("required secret %q is not bound", secretName)
		}

		if !hasBinding {
			continue
		}

		secretValue, exists, err := secretstore.GetSecret(boundSecretName)
		if err != nil {
			return nil, fmt.Errorf("failed to get secret %q: %w", boundSecretName, err)
		}

		if !exists {
			if !secretMeta.Optional {
				return nil, fmt.Errorf("required secret %q is bound to %q which does not exist in secret store", secretName, boundSecretName)
			}
			continue
		}

		secretEnv[secretName] = secretValue
	}

	return secretEnv, nil
}
